package org.jugile.web2;

import java.io.ByteArrayInputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;
import org.jugile.util.*;

public class Cycle extends Jugile {
	static Logger log = Logger.getLogger(Cycle.class);
	
	private Request req;
	private Component ctrl;
	private IStore store;

	private Object httpReq;
	private Object httpResp;
	protected void setHttpReq(Object o) { httpReq = o; }
	public Object getHttpReq() { return httpReq; }
	protected void setHttpResp(Object o) { httpResp = o; }
	public Object getHttpResp() { return httpResp; }
	
	protected String hostname = null;
	protected String port = null;
	protected String ipaddress = null;
	protected String remoteIp = "---";
	
	public String getHostname() { return hostname; }
	public String getPort() { return port; }
	public String getIpaddress() { return ipaddress; }
	public String getRemoteIp() { return remoteIp; }
		
	/**
	 * Some HTTP response status codes
	 */
	public static final String
		HTTP_OK = "200 OK",
		HTTP_REDIRECT = "301 Moved Permanently",
		HTTP_TEMP_REDIRECT = "307 Temporary Redirect",
		HTTP_FORBIDDEN = "403 Forbidden",
		HTTP_NOTFOUND = "404 Not Found",
		HTTP_BADREQUEST = "400 Bad Request",
		HTTP_INTERNALERROR = "500 Internal Server Error",
		HTTP_NOTIMPLEMENTED = "501 Not Implemented";

	/**
	 * Common mime types for dynamic content
	 */
	public static final String
		MIME_PLAINTEXT = "text/plain",
		MIME_HTML = "text/html",
		MIME_XML = "text/xml",
		MIME_JSON = "application/json;charset=utf-8",
		MIME_JS = "application/javascript",
		MIME_DEFAULT_BINARY = "application/octet-stream";



	public void debugRequest() {
		String pms = "";
		for (String key : req.getParamNames()) {
			pms += (String)key + "=" + req.getParam(key) + "&"; 
		}
		log.debug("request: " + req.getMethod() + ": "+ req.getURI());
		log.debug("<pms>"+ pms + "</pms>");
	}

	private VeloEngine ve() { return VeloEngine.getVe(); } 

	public Object getVar(String name) { return ve().getVar(name); }
	public void setVar(String name, Object o) { ve().setVar(name, o); }

	private Boolean isSyncServe = null;
	public boolean isSyncServe() {
		if (isSyncServe == null) {
			if ("true".equals(Props.get("jugile.web.syncServe"))) isSyncServe = true;
			else isSyncServe = false;
		}
		return isSyncServe;
	}
	
	public Response serve(Request request, IStore store) {
		if (isSyncServe()) return serveSync(request,store);
		return serve2(request,store);
	}
	public synchronized Response serveSync(Request request, IStore store) {
		return serve2(request,store);
	}
	
	public Response serve2(Request request, IStore store) {
		// internal worker request
		if (request == null && store == null) {
			Proxy app = new Proxy(getController("App"));
			app.call("worker");
			return null;
		}
		// check cache first
		Response r = null;
		if (!request.isForCaching()) {
			r = getCachePage(request.getMethod(), request.getURI());
			if (r != null) {
				log.info("serving page from cache: " + request.getURI());
				return r;
			}
		}
		
		// transaction
		this.store = store;
		IDomain d = domain();
		req = request;
		String info = request.getURI();
		Map<String,String> pms = request.getParams();
		if (pms.size() > 0) {
			info += "?";
			boolean first = true;
			for (String key : pms.keySet()) {
				if ("password".equals(key)) continue; // do not log passwords
				if (!first) info += "&";
				info += key +"=" +pms.get(key);
				first = false;
			}
		}
		Timer t = new Timer();
		try {
			log.info("doServe");
			r = doServe();
			if (d != null) d.commit();
		} catch (Exception e) {
			log.error("doing rollback");
			if (d != null) d.rollback();
			//log.error("error. doing rollback. ", e);
			// error page
			if (e instanceof HttpError) {
				r = getStatusResponse(((HttpError)e).getStatus(), e.getMessage());
				return r;
			}
			
			r = getStatusResponse(Cycle.HTTP_INTERNALERROR, e.getMessage());
			log.warn("could not handle request: "+request.getURI(), e);			
		} finally {
			String re = "REQ";
			if (req.isForCaching()) re = "CACHE";
			log.info(re + " "+getRemoteIp()+": " + info + " took ms: " + t.stop()
					+ " status: " + ((r==null)?"---":r.status));
		}
		return r;
	}

	private Response getStatusResponse(String status, String msg) {
		String fname = null;
		String txt = null;
		if (Cycle.HTTP_NOTFOUND.equals(status)) {
			fname = Props.get("jugile.web.404");
			txt = status;
		}
		if (Cycle.HTTP_INTERNALERROR.equals(status)) {
			fname = Props.get("jugile.web.500");
			txt = status;
		}
		if (!empty(fname)) {
			//Blob b = new Blob(fname);
			//txt = b.toString();
		}
		if (!empty(msg)) {
			txt = txt + "<br/>" + msg;
		}
		return new Response(status,"text/html;charset=utf-8",txt);
	}

	public Response doServe() {
		String res = null;

		ve().init();
		debugRequest();
		
		// resolve page
		Proxy a = new Proxy(getController("App"));
		ctrl = (Component)a.call("getPage", req, this);
		
		if (redirect != null) return redirectResponse();
		if (ctrl == null) {
			String n = req.getURI().substring(1);
			try { ctrl = (Component)getController(n); } catch (Exception e) {}
			if (ctrl == null) {
				log.warn("controller not found: " + n);
				return getStatusResponse(Cycle.HTTP_NOTFOUND, n);
			}
		}
		ctrl.init(this);

		// default actions
		Proxy c = new Proxy(ctrl);

		Response r = null;
		if ("GET".equals(req.getMethod())) r = (Response)c.call("get");
		if ("POST".equals(req.getMethod())) r = (Response)c.call("post");
		if ("PUT".equals(req.getMethod())) r = (Response)c.call("put");
		if (r != null) return r;
		
		r = new Response();
		
		String mime = ctrl.getMimetype();
		if (mime != null) r.mimeType = mime;
		else r.mimeType = MIME_HTML;
		
		String status = ctrl.getStatus();
		if (status != null) r.status = status;
		
		if (!empty(getParam("a"))) c.call("action"+up(getParam("a")));
		for (String act : getParamNames("a_")) {
			req.setParam("arg", getParam(act));
			c.call("action"+up(act.substring(2)));
		}
		
		// action can cause redirect
		if (redirect != null) return redirectResponse();
		
		res = ctrl.getContent(ve());
		if (res != null) {
			// direct content from controller
			if (ctrl.getMimetype() != null) r.mimeType = ctrl.getMimetype();
			else r.mimeType = MIME_HTML;
		} else {
			String tmpl = ctrl.getTmpl();
			if (tmpl == null) {
				res = (String)(new Proxy(ctrl)).call("data");
				r.data = new ByteArrayInputStream(res.getBytes());
				if (ctrl.getMimetype() != null) r.mimeType = ctrl.getMimetype();
				else r.mimeType = MIME_PLAINTEXT;
				return r;
			}	
			Blob b = new Blob(tmpl);
			ve().put("c", ctrl);
			String body = ve().eval2(b);
			ve().put("body", body);
			b = new Blob(ctrl.getBorder((String)ve().getVar("Border")));
			res = ve().eval(b);
		}
		r.data = new ByteArrayInputStream(res.getBytes());
		String mime2 = (String)ve().get("mimetype");
		if (!empty(mime2)) r.mimeType = mime2;
		return r;
	}

	private Response redirectResponse() {
		Response r = getRedirectResponse(redirect);
		redirect = null;
		return r;
	}

	public Response getRedirectPermanentlyResponse(String url) {
		Response r = new Response(HTTP_REDIRECT,"","");
		r.addHeader("Location",url);
		return r;
	}
	public Response getRedirectResponse(String url) {
		Response r = new Response(HTTP_TEMP_REDIRECT,"","");
		r.addHeader("Location",url);
		return r;
	}
	
	private String redirect = null;
	public void redirect(String page) {
		log.debug("redirect: " + page);
		redirect = page;
	}

	// ei vaikuttanut suorituskykyyn template cache
	private static Map<String,Blob> templates = new HashMap<String,Blob>();
	private Blob getBlob(String name) {
		Blob b = templates.get(name);
		if (b != null) return b;
		b = new Blob(name);
		templates.put(name, b);
		return b;
	}

	private int includeCount = 0;
	public String evalTemplate(String fname, Object model) {
		Blob b = new Blob(fname);
		ve().put("includeModel",model);
		includeCount++;
		if (includeCount > 7) return null;
		String res = ve().eval2(b);
		includeCount--;
		return res;
	}

	// XXX: testing - remove duplicates when works
	public String evalString(String str, Object model) {
		Blob b = new Blob();
		b.append(nn(str));
		ve().put("m",model);
		String res = ve().eval2(b);
		return res;
	}

	

	protected static String getPackage() { return Props.getWebPackage();	}
	private String getTemplatePath() { return Props.getTemplatePath(); }
	
	public static Object getController(String name) {
		if (empty(name)) return null;
		name = name.replaceAll("/", ".");
		int lastdot = name.lastIndexOf(".");
		if (lastdot < 0) lastdot = 0;
		else lastdot++;
		if (Character.isLowerCase(name.charAt(lastdot))) {
			String p1 = name.substring(0,lastdot);
			String p2 = name.substring(lastdot);
			p2 = up(p2);
			name = p1+p2;
		}
		return instantiateObject(getPackage() + "." + name);
	}
	
	public String getUri() { return req.getURI(); }	
	public String getMethod() { return req.getMethod(); }
	public String getBody() { return req.getBody(); }
	//public String getCtxPath() { return req.getCtxPath(); }
	
	// ----- cycle -----

	public String getParam(String key) { return req.getParam(key); }
	public List<String> getParamNames() { return req.getParamNames(); }
	public List<String> getParamNames(String prefix) { return req.getParamNames(prefix); }
	public Blob getFile(String key) { return req.getFile(key); }
	public List<String> getFileNames() { return req.getFileNames(); }
	public List<String> getParamValues(String key) { return req.getParamValues(key); }
	
	//public String getContextPath() { return contextPath; }
	//public void setContextPath(String v) { contextPath = v; }
	//public boolean isServlet() { return req.isServlet(); }
	
	public void put(String key, String value) { if (store != null) store.put(key, value); }
	public String get(String key) { return store == null?null:(String)store.get(key); }
	public Object getObject(String key) { return store == null?null:store.get(key);}
	public void putObject(String key, Object value) { if (store != null) store.put(key, value);}
	
	// ---- cycle attributes for one req cycle ----
	private Map<String,Object> attrs = new HashMap<String,Object>();
	public Object attr(String key) { return attrs.get(key); }
	public void attr(String key, Object value) { attrs.put(key, value); }
	public Map<String,Object> getAttrs() { return attrs; }
	
	// ---- application managed page cache ----
	// TODO: check optimal syncronization
	private static Map<String,Response> pageCache = new HashMap<String,Response>();
	public static void cachePage(String method, String uri) {
		Cycle c = new Cycle();
		Request req = new Request();
		req.setURI(uri);
		req.setMethod(method);
		req.setForCaching();
		Response resp = c.serve(req, null);
		resp.cache();
		pageCache.put(method+":"+uri, resp);
		// write it to file for testing
		//Blob b = new Blob();
		//b.append(data);
		//b.write("testfile.html");		
	}
	public static Response getCachePage(String method, String uri) {
		Response res = pageCache.get(method+":"+uri);
		if (res != null) res = res.decache();
		return res;
	}
	public static void flushPage(String method, String uri) {
		pageCache.remove(method+":"+uri);
	}

	// ----- static data (app context) ------
	private static String ctxPath;
	protected static void setCtxPath(String v) { ctxPath = v; }
	public static String getCtxPath() { return ctxPath; }
	public static boolean isServlet() { return !empty(ctxPath); }

	// don't make this synchronized - blocks worker thread
	public static void appInit() {
		Proxy a = new Proxy(getController("App"));
		try {
			a.call("init");
		} catch (Exception e) {
			log.fatal("could not do init.", e);
			fail(e);
		}
	}


	// ----- for testing -----
	public static void test(Component comp) {
		Cycle c = new Cycle();
		c.req = new Request();
		c.store = new DummyStore(); 
		comp.init(c);
	}
	public void setParam(String key, String value) { req.setParam(key, value); }
	public static class DummyStore implements IStore {
		private Map<String,Object> map = new HashMap<String,Object>(); 
		public Object get(String key) { return map.get(key); }
		public void put(String key, Object value) { map.put(key, value); }
	}


	// ----- props -----

	
	// ----- domain -----
	public IDomain domain() {
		return getDomainInstance();
	}


}
